﻿// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.ComponentModel;
using System.ComponentModel.Design;
using System.Drawing;

namespace System.Windows.Forms.Design;

/// <summary>
///  This is an abstract base class that encapsulates a lot of
///  the details of handling selection drags. Just about everyone
///  that implements a selection UI handler will extend this.
/// </summary>
internal abstract class SelectionUIHandler
{
    // These handle our drag feedback. These come into play when we're
    // actually moving components around. The selection UI service
    // dictates when this happens.
    //
    private Rectangle _dragOffset = Rectangle.Empty;        // this gets added to a component's x, y, width, height
    private Control[]? _dragControls;                       // the set of controls we're dragging
    private BoundsInfo[]? _originalCoordinates;             // the saved coordinates of the components we're dragging.
    private SelectionRules _rules;                          // the rules of the current drag.

    private const int MinControlWidth = 3;
    private const int MinControlHeight = 3;

    /// <summary>
    ///  Begins a drag operation. A designer should examine the list of components
    ///  to see if it wants to support the drag. If it does, it should return
    ///  true. If it returns true, the designer should provide
    ///  UI feedback about the drag at this time. Typically, this feedback consists
    ///  of an inverted rectangle for each component, or a caret if the component
    ///  is text.
    /// </summary>
    public virtual bool BeginDrag(object[] components, SelectionRules rules, int initialX, int initialY)
    {
        _dragOffset = default;
        _originalCoordinates = null;
        _rules = rules;

        _dragControls = new Control[components.Length];
        for (int i = 0; i < components.Length; i++)
        {
            Debug.Assert(components[i] is IComponent, "Selection UI handler only deals with IComponents");
            _dragControls[i] = GetControl((IComponent)components[i]);
            Debug.Assert(_dragControls[i] is not null, "Everyone must have a control");
        }

        return true;
    }

    /// <summary>
    ///  This is called by MoveControls when the user has requested that the move be
    ///  cancelled. This puts all the controls back to where they were.
    /// </summary>
    private static void CancelControlMove(Control[] controls, BoundsInfo[] bounds)
    {
        Debug.Assert(bounds is not null && controls is not null && bounds.Length == controls.Length, "bounds->controls mismatch");

        Rectangle b = default;

        // Whip through each of the controls.
        //
        for (int i = 0; i < controls.Length; i++)
        {
            Control? parent = controls[i].Parent;

            // Suspend parent layout so that we don't continuously re-arrange components
            // while we move.
            //
            parent?.SuspendLayout();

            b.X = bounds[i].X;
            b.Y = bounds[i].Y;
            b.Width = bounds[i].Width;
            b.Height = bounds[i].Height;

            controls[i].Bounds = b;
        }

        // And resume their layout
        //
        for (int i = 0; i < controls.Length; i++)
        {
            Control? parent = controls[i].Parent;
            parent?.ResumeLayout();
        }
    }

    /// <summary>
    ///  Called when the user has moved the mouse. This will only be called on
    ///  the designer that returned true from beginDrag. The designer
    ///  should update its UI feedback here.
    /// </summary>
    public virtual void DragMoved(object[] components, Rectangle offset)
    {
        _dragOffset = offset;
        MoveControls(components, false, false);
        Debug.Assert(_originalCoordinates is not null, "We are keying off of originalCoords, but MoveControls didn't set it");
    }

    /// <summary>
    ///  Called when the user has completed the drag. The designer should
    ///  remove any UI feedback it may be providing.
    /// </summary>
    public virtual void EndDrag(object[] components, bool cancel)
    {
        try
        {
            MoveControls(components, cancel, true);
        }
        catch (CheckoutException checkoutEx)
        {
            if (checkoutEx == CheckoutException.Canceled)
            {
                MoveControls(components, true, false);
            }
            else
            {
                throw;
            }
        }
    }

    /// <summary>
    ///  Retrieves the base component for the selection handler.
    /// </summary>
    protected abstract IComponent GetComponent();

    /// <summary>
    ///  Retrieves the base component's UI control for the selection handler.
    /// </summary>
    protected abstract Control GetControl();

    /// <summary>
    ///  Retrieves the UI control for the given component.
    /// </summary>
    protected abstract Control GetControl(IComponent component);

    /// <summary>
    ///  Retrieves the current grid snap size we should snap objects
    ///  to.
    /// </summary>
    protected abstract Size GetCurrentSnapSize();

    /// <summary>
    ///  We use this to request often-used services.
    /// </summary>
    protected abstract object? GetService(Type serviceType);

    protected T? GetService<T>() where T : class => GetService(typeof(T)) as T;

    protected bool TryGetService<T>([NotNullWhen(true)] out T? service) where T : class
    {
        service = GetService(typeof(T)) as T;
        return service is not null;
    }

    /// <summary>
    ///  Determines if the selection UI handler should attempt to snap
    ///  objects to a grid.
    /// </summary>
    protected abstract bool GetShouldSnapToGrid();

    /// <summary>
    ///  Given a rectangle, this updates the dimensions of it
    ///  with any grid snaps and returns a new rectangle. If
    ///  no changes to the rectangle's size were needed, this
    ///  may return the same rectangle.
    /// </summary>
    public abstract Rectangle GetUpdatedRect(Rectangle orignalRect, Rectangle dragRect, bool updateSize);

    /// <summary>
    ///  Called when we need to move the controls on our frame while dragging. This
    ///  can perform three operations:  It can update the current controls location,
    ///  it can commit the new controls (final move), and it can roll back a movement
    ///  to the beginning of the operation.
    /// </summary>
    private void MoveControls(object[] components, bool cancel, bool finalMove)
    {
        Control[] controls = _dragControls!;
        Rectangle offset = _dragOffset;
        BoundsInfo[] bounds = _originalCoordinates!;
        Point adjustedLoc = default;

        // Erase the clipping and other state if this is the final move.
        //
        if (finalMove)
        {
            Cursor.Clip = Rectangle.Empty;
            _dragOffset = Rectangle.Empty;
            _dragControls = null;
            _originalCoordinates = null;
        }

        // If we haven't started to move yet, there's nothing to do.
        //
        if (offset.IsEmpty)
        {
            return;
        }

        // Short circuit the work if we didn't move anything.
        // This will prevent a change notification for just
        // selecting components.
        //
        if (finalMove && offset is { X: 0, Y: 0, Width: 0, Height: 0 })
        {
            return;
        }

        // If cancel was specified, we must roll all controls back to their original positions.
        //
        if (cancel)
        {
            CancelControlMove(controls, bounds);
            return;
        }

        // We must keep track of the original coordinates of each control, just in case
        // the user cancels out of moving them. So, we create a "BoundsInfo" object for
        // each control that saves this state.
        //
        if (_originalCoordinates is null && !finalMove)
        {
            _originalCoordinates = new BoundsInfo[controls.Length];
            for (int i = 0; i < controls.Length; i++)
            {
                _originalCoordinates[i] = new BoundsInfo(controls[i]);
            }

            bounds = _originalCoordinates;
        }

        // Two passes here. First pass suspends all parent layout and updates the
        // component positions. Second pass re-enables layout.
        //
        for (int i = 0; i < controls.Length; i++)
        {
            Debug.Assert(controls[i] == GetControl((IComponent)components[i]), "Control->Component mapping is out of sync");

            Control? parent = controls[i].Parent;

            // Suspend parent layout so that we don't continuously re-arrange components
            // while we move.
            //
            parent?.SuspendLayout();

            BoundsInfo ctlBounds = bounds[i];

            adjustedLoc.X = ctlBounds.lastRequestedX;
            adjustedLoc.Y = ctlBounds.lastRequestedY;

            if (!finalMove)
            {
                ctlBounds.lastRequestedX += offset.X;
                ctlBounds.lastRequestedY += offset.Y;
                ctlBounds.lastRequestedWidth += offset.Width;
                ctlBounds.lastRequestedHeight += offset.Height;
            }

            // Our "target" values are the ones we would like to set
            // the control to. We may modify them if they would mke the control
            // size negative or zero, however.
            //
            int targetX = ctlBounds.lastRequestedX;
            int targetY = ctlBounds.lastRequestedY;
            int targetWidth = ctlBounds.lastRequestedWidth;
            int targetHeight = ctlBounds.lastRequestedHeight;

            Rectangle oldBounds = controls[i].Bounds;

            if ((_rules & SelectionRules.Moveable) == 0)
            {
                // We use the minimum size of either a grid snap or 1
                //
                Size minSize;

                if (GetShouldSnapToGrid())
                {
                    minSize = GetCurrentSnapSize();
                }
                else
                {
                    minSize = new Size(1, 1);
                }

                if (targetWidth < minSize.Width)
                {
                    targetWidth = minSize.Width;
                    targetX = oldBounds.X;
                }

                if (targetHeight < minSize.Height)
                {
                    targetHeight = minSize.Height;
                    targetY = oldBounds.Y;
                }
            }

            // Bug #72905  <subhag> Form X,Y defaulted to (0,0)
            //

            IDesignerHost? host = GetService<IDesignerHost>();
            Debug.Assert(host is not null, "No designer host");
            if (controls[i] == host.RootComponent)
            {
                targetX = 0;
                targetY = 0;
            }

            // Adjust our target dimensions by the grid snaps
            //
            Rectangle tempNewBounds = GetUpdatedRect(oldBounds, new Rectangle(targetX, targetY, targetWidth, targetHeight), true);
            Rectangle newBounds = oldBounds;

            // Now apply the correct values to newBounds -- we only want to apply the new value if our
            // selection rules dictate so.
            //
            if ((_rules & SelectionRules.Moveable) != 0)
            {
                newBounds.X = tempNewBounds.X;
                newBounds.Y = tempNewBounds.Y;
            }
            else
            {
                if ((_rules & SelectionRules.TopSizeable) != 0)
                {
                    newBounds.Y = tempNewBounds.Y;
                    newBounds.Height = tempNewBounds.Height;
                }

                if ((_rules & SelectionRules.BottomSizeable) != 0)
                {
                    newBounds.Height = tempNewBounds.Height;
                }

                if ((_rules & SelectionRules.LeftSizeable) != 0)
                {
                    newBounds.X = tempNewBounds.X;
                    newBounds.Width = tempNewBounds.Width;
                }

                if ((_rules & SelectionRules.RightSizeable) != 0)
                {
                    newBounds.Width = tempNewBounds.Width;
                }
            }

            bool locChanged = (offset.X != 0 || offset.Y != 0);
            bool sizeChanged = (offset.Width != 0 || offset.Height != 0);

            // If both the location and size changed, attempt to update the control in
            // one step. This will prevent flicker.
            //
            if (locChanged && sizeChanged)
            {
                // We shouldn't care if we're directly manipulating the control or not during
                // the move. For perf, just call directly on the control. For the final
                // move, however, we must go through the property descriptor.
                //
                PropertyDescriptor? boundsProp = TypeDescriptor.GetProperties(components[i])["Bounds"];

                if (boundsProp is not null && !boundsProp.IsReadOnly)
                {
                    if (finalMove)
                    {
                        object component = components[i];
                        boundsProp.SetValue(component, newBounds);
                    }
                    else
                    {
                        controls[i].Bounds = newBounds;
                    }

                    // Now reset the loc and size changed flags so
                    // we don't try to change again.
                    //
                    locChanged = sizeChanged = false;
                }
            }

            // Adjust the location property with the new value. ONLY do this if the offset
            // is nonzero, however. Otherwise we may get round-off errors that can cause flicker.
            // Plus, why set the property if it shouldn't be?
            //
            if (locChanged)
            {
                adjustedLoc.X = newBounds.X;
                adjustedLoc.Y = newBounds.Y;

                PropertyDescriptor? trayProp = TypeDescriptor.GetProperties(components[i])["TrayLocation"];

                if (trayProp is not null && !trayProp.IsReadOnly)
                {
                    trayProp.SetValue(components[i], adjustedLoc);
                }
                else
                {
                    // We shouldn't care if we're directly manipulating the control or not during
                    // the move. For perf, just call directly on the control. For the final
                    // move, however, we must go through the property descriptor.
                    //
                    PropertyDescriptor? leftProp = TypeDescriptor.GetProperties(components[i])["Left"];
                    PropertyDescriptor? topProp = TypeDescriptor.GetProperties(components[i])["Top"];
                    if (topProp is not null && !topProp.IsReadOnly)
                    {
                        if (finalMove)
                        {
                            object component = components[i];
                            topProp.SetValue(component, adjustedLoc.Y);
                        }
                        else
                        {
                            controls[i].Top = adjustedLoc.Y;
                        }
                    }

                    if (leftProp is not null && !leftProp.IsReadOnly)
                    {
                        if (finalMove)
                        {
                            object component = components[i];
                            leftProp.SetValue(component, adjustedLoc.X);
                        }
                        else
                        {
                            controls[i].Left = adjustedLoc.X;
                        }
                    }

                    if (leftProp is null || topProp is null)
                    {
                        PropertyDescriptor? locationProp = TypeDescriptor.GetProperties(components[i])["Location"];
                        if (locationProp is not null && !locationProp.IsReadOnly)
                        {
                            locationProp.SetValue(components[i], adjustedLoc);
                        }
                    }
                }
            }

            // Adjust the size property with the new value. ONLY do this if the offset
            // is nonzero, however. Otherwise we may get round-off errors that can cause flicker.
            // Plus, why set the property if it shouldn't be?
            //
            if (sizeChanged)
            {
                // If you are tempted to hoist this "new" out of the loop, don't. The undo
                // unit below just holds a reference to it.
                //
                Size size = new(Math.Max(MinControlWidth, newBounds.Width), Math.Max(MinControlHeight, newBounds.Height));

                PropertyDescriptor? widthProp = TypeDescriptor.GetProperties(components[i])["Width"];
                PropertyDescriptor? heightProp = TypeDescriptor.GetProperties(components[i])["Height"];

                if (widthProp is not null && !widthProp.IsReadOnly && size.Width != (int)widthProp.GetValue(components[i])!)
                {
                    if (finalMove)
                    {
                        object component = components[i];
                        widthProp.SetValue(component, size);
                    }
                    else
                    {
                        controls[i].Width = size.Width;
                    }
                }

                if (heightProp is not null && !heightProp.IsReadOnly && size.Height != (int)heightProp.GetValue(components[i])!)
                {
                    if (finalMove)
                    {
                        object component = components[i];
                        heightProp.SetValue(component, size);
                    }
                    else
                    {
                        controls[i].Height = size.Height;
                    }
                }
            }
        }

        // Now resume the parent layouts
        //
        for (int i = 0; i < controls.Length; i++)
        {
            Control? parent = controls[i].Parent;

            if (parent is not null)
            {
                parent.ResumeLayout();
                parent.Update();
            }

            controls[i].Update();
        }
    }

    /// <summary>
    ///  Queries to see if a drag operation is valid on this handler for the given set of components.
    ///  If it returns true, BeginDrag will be called immediately after.
    /// </summary>
    public bool QueryBeginDrag(object[] components)
    {
        if (TryGetService(out IComponentChangeService? cs))
        {
            try
            {
                if (components is not null && components.Length > 0)
                {
                    foreach (object c in components)
                    {
                        cs.OnComponentChanging(c, TypeDescriptor.GetProperties(c)["Location"]);
                        PropertyDescriptor? sizeProp = TypeDescriptor.GetProperties(c)["Size"];

                        if (sizeProp is not null && sizeProp.Attributes.Contains(DesignerSerializationVisibilityAttribute.Hidden))
                        {
                            sizeProp = TypeDescriptor.GetProperties(c)["ClientSize"];
                        }

                        cs.OnComponentChanging(c, sizeProp);
                    }
                }
                else
                {
                    cs.OnComponentChanging(GetComponent(), null);
                }
            }
            catch (CheckoutException coEx) when (coEx == CheckoutException.Canceled)
            {
                // cancel the drag.
                return false;
            }
            catch (InvalidOperationException)
            {
                // If the doc data for this designer is currently locked, we will get an exception here. In this
                // case, just eat the exception. If this exception was thrown by someone else, that's fine too.
                // No point in bubbling exceptions here anyway.

                return false;
            }
        }

        return components is not null && components.Length > 0;
    }

    /// <summary>
    ///  Asks the handler to set the appropriate cursor
    /// </summary>
    public abstract void SetCursor();

    public virtual void OleDragEnter(DragEventArgs de)
    {
    }

    public virtual void OleDragDrop(DragEventArgs de)
    {
    }

    public virtual void OleDragOver(DragEventArgs de)
    {
    }

    public virtual void OleDragLeave()
    {
    }

    /// <summary>
    ///  This class holds bounds information for controls that are being moved.
    /// </summary>
    private class BoundsInfo
    {
        public int X;                           // Original saved X
        public int Y;                           // Original saved Y
        public int Width;                       // Original saved width
        public int Height;                      // Original saved height
        public int lastRequestedX = -1;
        public int lastRequestedY = -1;
        public int lastRequestedWidth = -1;
        public int lastRequestedHeight = -1;

        /// <summary>
        ///  Creates and initializes a new BoundsInfo object.
        /// </summary>
        public BoundsInfo(Control control)
        {
            // get the size & loc from the props so the designers can adjust them.
            //
            PropertyDescriptor? sizeProp = TypeDescriptor.GetProperties(control)["Size"];
            PropertyDescriptor? locProp = TypeDescriptor.GetProperties(control)["Location"];

            Size sz;
            Point loc;

            if (sizeProp is not null)
            {
                sz = (Size)sizeProp.GetValue(control)!;
            }
            else
            {
                sz = control.Size;
            }

            if (locProp is not null)
            {
                loc = (Point)locProp.GetValue(control)!;
            }
            else
            {
                loc = control.Location;
            }

            X = loc.X;
            Y = loc.Y;
            Width = sz.Width;
            Height = sz.Height;
            lastRequestedX = X;
            lastRequestedY = Y;
            lastRequestedWidth = Width;
            lastRequestedHeight = Height;
        }

        /// <summary>
        ///  Overrides object ToString.
        /// </summary>
        public override string ToString()
        {
            return $"{{X={X}, Y={Y}, Width={Width}, Height={Height}}}";
        }
    }
}
